<?php
/**
 * Classes utilitárias para a aplicação
 * @author		João Batista Neto
 * @package		rpo
 * @subpackage	util
 */
namespace rpo\util;

use \ArrayAccess;
use rpo\base\Object;
use rpo\base\exception\CastException;
use rpo\base\exception\IndexOutOfBoundsException;
use rpo\base\exception\UndefinedIndexException;

/**
 * Essa classe é o esqueleto de uma implementação da interface Lists.
 * @abstract
 * @author		João Batista Neto
 * @package		rpo
 * @subpackage	util
 * @category	list
 */
abstract class AbstractLists extends AbstractCollection implements ArrayAccess {
	/**
	 * Limite da lista
	 * @var integer
	 */
	protected $limit = 0;

	/**
	 * Adiciona um novo objeto à coleção
	 * @param Object $o
	 * @return boolean
	 * @throws CastException Se o objeto for de um tipo diferente da lista
	 */
	public function add( Object $o ){
		$ret = parent::add( $o );

		if ( $ret ) ++$this->limit;

		return $ret;
	}

	/**
	 * Verifica os limites da lista
	 * @param integer $offset
	 * @return boolean
	 */
	protected function boundsCheck( $offset ){
		$ret = false;

		if ( is_int( $offset ) ){
			if ( ( $offset < 0 ) || ( $offset > $this->limit - 1 ) ){
				throw new IndexOutOfBoundsException( sprintf( 'Offset[ %d ] está fora dos limites da lista.' , $offset ) );
			} elseif ( !isset( $this->storage[ $offset ] ) ){
				throw new UndefinedIndexException( sprintf( 'Offset[ %d ] não está definido.' , $offset ) );
			} else {
				$ret = true;
			}
		} else {
			throw new CastException( sprintf( '%s espera um offset inteiro, %s foi dado.' , __METHOD__ , gettype( $offset ) ) );
		}

		return $ret;
	}

	/**
	 * Recupera um elemento associado ao $offset na lista
	 * @param integer $offset
	 * @return Object
	 * @throws CastException Se $offset não for um inteiro
	 * @throws IndexOutOfBoundsException Se $offset for menor que 0 ou maior que o limite da lista
	 * @throws UndefinedIndexException Se o $offset não tiver sido definido
	 */
	public function get( $offset ){
		return $this[ $offset ];
	}

	/**
	 * Recupera a posição do elemento na lista
	 * @param Object $o
	 * @return integer
	 * @see Lists::get()
	 */
	public function indexOf( Object $o ){
		foreach ( $this->getIterator() as $offset => $item ){
			if ( $item->equals( $o ) ){
				return $offset;
			}
		}

		return -1;
	}

	/**
	 * Recupera a última posição do elemento na lista
	 * @param Object $o
	 * @return integer
	 * @see Lists::indexOf()
	 */
	public function lastIndexOf( Object $o ){
		$iterator = $this->getIterator();
		$offset   = $iterator->count() - 1;

		while ( $offset >= 0 ){
			$iterator->seek( $offset );

			if ( $iterator->current()->equals( $o ) ){
				return $offset;
			}

			--$offset;
		}

		return -1;
	}

	/**
	 * Verifica se um offset está definido
	 * @param integer $offset
	 * @return boolean
	 */
	public function offsetExists( $offset ){
		$ret = false;

		try {
			if ( $this->boundsCheck( $offset ) ){
				$ret = true;
			}
		} catch ( UndefinedIndexException $e ){
			$ret = false;
		}

		return $ret;
	}

	/**
	 * Recupera um elemento associado ao $offset na lista
	 * @param integer $offset
	 * @return Object
	 * @throws CastException Se $offset não for um inteiro
	 * @throws IndexOutOfBoundsException Se $offset for menor que 0 ou maior que o limite da lista
	 * @throws UndefinedIndexException Se o $offset não tiver sido definido
	 */
	public function offsetGet( $offset ){
		if ( $this->boundsCheck( $offset ) ){
			return $this->storage[ $offset ];
		}
	}

	/**
	 * Redefine um elemento na posição $offset
	 * @param integer $offset
	 * @param Object $value
	 * @return boolean
	 */
	public function offsetSet( $offset , $value ){
		$ret = false;

		if ( is_null( $offset ) ){
			$offset = $this->limit;
		}

		if ( $this->typeTest( $value ) ){
			try {
				$ret = $this->boundsCheck( $offset );
			} catch ( UndefinedIndexException $e ){
				$ret = true;
				++$this->limit;
			}

			if ( $ret ){
				$this->storage[ $offset ] = $value;
			}
		}

		return $ret;
	}

	/**
	 * Remove um offset da lista
	 * @param integer $offset
	 */
	public function offsetUnset( $offset ){
		if ( $this->boundsCheck( $offset ) ){
			$this->storage->offsetUnset( $offset );
		}
	}

	/**
	 * Redefine um elemento na posição $offset
	 * @param integer $offset
	 * @param Object $o
	 * @return Object
	 * @see AbstractLists::offsetSet
	 * @throws CastException Se $offset não for um inteiro
	 * @throws IndexOutOfBoundsException Se $offset for menor que 0 ou maior que o limite da lista
	 */
	public function set( $offset , Object $o ){
		$this[ $offset ] = $o;

		return $o;
	}

	/**
	 * Cria uma sub-lista da lista atual
	 * @param $fromIndex
	 * @param $toIndex
	 * @return Lists
	 */
	public function subList( $fromIndex , $toIndex ){
		if ( $this->boundsCheck( $fromIndex ) && $this->boundsCheck( $toIndex ) ){
			$iterator = $this->getIterator();
			$iterator->seek( $fromIndex );

			$list = $this->getClass()->newInstance();

			for ( $i = $fromIndex ; $iterator->valid() && $i <= $toIndex ; ++$i ){
				$list->add( $iterator->current() );
				$iterator->next();
			}

			return $list;
		}
	}
}